1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.solr.schema;
19  
20  import javax.xml.xpath.XPath;
21  import javax.xml.xpath.XPathConstants;
22  import javax.xml.xpath.XPathExpressionException;
23  import java.io.IOException;
24  import java.io.Writer;
25  import java.lang.invoke.MethodHandles;
26  import java.util.*;
27  import java.util.regex.Pattern;
28  
29  import org.apache.lucene.analysis.Analyzer;
30  import org.apache.lucene.analysis.DelegatingAnalyzerWrapper;
31  import org.apache.lucene.index.DocValuesType;
32  import org.apache.lucene.index.FieldInfo;
33  import org.apache.lucene.index.IndexOptions;
34  import org.apache.lucene.index.IndexReader;
35  import org.apache.lucene.index.IndexableField;
36  import org.apache.lucene.index.MultiFields;
37  import org.apache.lucene.search.similarities.Similarity;
38  import org.apache.lucene.uninverting.UninvertingReader;
39  import org.apache.lucene.util.Version;
40  import org.apache.solr.common.SolrException;
41  import org.apache.solr.common.SolrException.ErrorCode;
42  import org.apache.solr.common.params.SolrParams;
43  import org.apache.solr.common.util.NamedList;
44  import org.apache.solr.common.util.SimpleOrderedMap;
45  import org.apache.solr.core.Config;
46  import org.apache.solr.core.SolrConfig;
47  import org.apache.solr.core.SolrResourceLoader;
48  import org.apache.solr.request.LocalSolrQueryRequest;
49  import org.apache.solr.response.SchemaXmlWriter;
50  import org.apache.solr.response.SolrQueryResponse;
51  import org.apache.solr.search.similarities.ClassicSimilarityFactory;
52  import org.apache.solr.util.DOMUtil;
53  import org.apache.solr.util.plugin.SolrCoreAware;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  import org.w3c.dom.Document;
57  import org.w3c.dom.Element;
58  import org.w3c.dom.NamedNodeMap;
59  import org.w3c.dom.Node;
60  import org.w3c.dom.NodeList;
61  import org.xml.sax.InputSource;
62  
63  import static java.util.Collections.singletonList;
64  import static java.util.Collections.singletonMap;
65  
66  /**
67   * <code>IndexSchema</code> contains information about the valid fields in an index
68   * and the types of those fields.
69   *
70   *
71   */
72  public class IndexSchema {
73    public static final String COPY_FIELD = "copyField";
74    public static final String COPY_FIELDS = COPY_FIELD + "s";
75    public static final String DEFAULT_OPERATOR = "defaultOperator";
76    public static final String DEFAULT_SCHEMA_FILE = "schema.xml";
77    public static final String DEFAULT_SEARCH_FIELD = "defaultSearchField";
78    public static final String DESTINATION = "dest";
79    public static final String DYNAMIC_FIELD = "dynamicField";
80    public static final String DYNAMIC_FIELDS = DYNAMIC_FIELD + "s";
81    public static final String FIELD = "field";
82    public static final String FIELDS = FIELD + "s";
83    public static final String FIELD_TYPE = "fieldType";
84    public static final String FIELD_TYPES = FIELD_TYPE + "s";
85    public static final String INTERNAL_POLY_FIELD_PREFIX = "*" + FieldType.POLY_FIELD_SEPARATOR;
86    public static final String LUCENE_MATCH_VERSION_PARAM = "luceneMatchVersion";
87    public static final String MAX_CHARS = "maxChars";
88    public static final String NAME = "name";
89    public static final String REQUIRED = "required";
90    public static final String SCHEMA = "schema";
91    public static final String SIMILARITY = "similarity";
92    public static final String SLASH = "/";
93    public static final String SOLR_QUERY_PARSER = "solrQueryParser";
94    public static final String SOURCE = "source";
95    public static final String TYPE = "type";
96    public static final String TYPES = "types";
97    public static final String UNIQUE_KEY = "uniqueKey";
98    public static final String VERSION = "version";
99  
100   private static final String AT = "@";
101   private static final String DESTINATION_DYNAMIC_BASE = "destDynamicBase";
102   private static final String SOLR_CORE_NAME = "solr.core.name";
103   private static final String SOURCE_DYNAMIC_BASE = "sourceDynamicBase";
104   private static final String SOURCE_EXPLICIT_FIELDS = "sourceExplicitFields";
105   private static final String TEXT_FUNCTION = "text()";
106   private static final String XPATH_OR = " | ";
107 
108   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
109   protected final SolrConfig solrConfig;
110   protected String resourceName;
111   protected String name;
112   protected float version;
113   protected final SolrResourceLoader loader;
114 
115   protected Map<String,SchemaField> fields = new HashMap<>();
116   protected Map<String,FieldType> fieldTypes = new HashMap<>();
117 
118   protected List<SchemaField> fieldsWithDefaultValue = new ArrayList<>();
119   protected Collection<SchemaField> requiredFields = new HashSet<>();
120   protected volatile DynamicField[] dynamicFields;
121   public DynamicField[] getDynamicFields() { return dynamicFields; }
122 
123   private Analyzer indexAnalyzer;
124   private Analyzer queryAnalyzer;
125 
126   protected List<SchemaAware> schemaAware = new ArrayList<>();
127 
128   protected String defaultSearchFieldName=null;
129   protected String queryParserDefaultOperator = "OR";
130   protected boolean isExplicitQueryParserDefaultOperator = false;
131 
132 
133   protected Map<String, List<CopyField>> copyFieldsMap = new HashMap<>();
134   public Map<String,List<CopyField>> getCopyFieldsMap() { return Collections.unmodifiableMap(copyFieldsMap); }
135   
136   protected DynamicCopy[] dynamicCopyFields;
137   public DynamicCopy[] getDynamicCopyFields() { return dynamicCopyFields; }
138 
139   /**
140    * keys are all fields copied to, count is num of copyField
141    * directives that target them.
142    */
143   protected Map<SchemaField, Integer> copyFieldTargetCounts = new HashMap<>();
144 
145     /**
146    * Constructs a schema using the specified resource name and stream.
147    * @see SolrResourceLoader#openSchema
148    * By default, this follows the normal config path directory searching rules.
149    * @see SolrResourceLoader#openResource
150    */
151   public IndexSchema(SolrConfig solrConfig, String name, InputSource is) {
152     assert null != solrConfig : "SolrConfig should never be null";
153     assert null != name : "schema resource name should never be null";
154     assert null != is : "schema InputSource should never be null";
155 
156     this.solrConfig = solrConfig;
157     this.resourceName = name;
158     loader = solrConfig.getResourceLoader();
159     try {
160       readSchema(is);
161       loader.inform(loader);
162     } catch (IOException e) {
163       throw new RuntimeException(e);
164     }
165   }
166   
167   /**
168    * @since solr 1.4
169    */
170   public SolrResourceLoader getResourceLoader() {
171     return loader;
172   }
173   
174   /** Gets the name of the resource used to instantiate this schema. */
175   public String getResourceName() {
176     return resourceName;
177   }
178 
179   /** Sets the name of the resource used to instantiate this schema. */
180   public void setResourceName(String resourceName) {
181     this.resourceName = resourceName;
182   }
183 
184   /** Gets the name of the schema as specified in the schema resource. */
185   public String getSchemaName() {
186     return name;
187   }
188   
189   /** The Default Lucene Match Version for this IndexSchema */
190   public Version getDefaultLuceneMatchVersion() {
191     return solrConfig.luceneMatchVersion;
192   }
193 
194   public float getVersion() {
195     return version;
196   }
197 
198 
199   /**
200    * Provides direct access to the Map containing all explicit
201    * (ie: non-dynamic) fields in the index, keyed on field name.
202    *
203    * <p>
204    * Modifying this Map (or any item in it) will affect the real schema
205    * </p>
206    * 
207    * <p>
208    * NOTE: this function is not thread safe.  However, it is safe to use within the standard
209    * <code>inform( SolrCore core )</code> function for <code>SolrCoreAware</code> classes.
210    * Outside <code>inform</code>, this could potentially throw a ConcurrentModificationException
211    * </p>
212    */
213   public Map<String,SchemaField> getFields() { return fields; }
214 
215   /**
216    * Provides direct access to the Map containing all Field Types
217    * in the index, keyed on field type name.
218    *
219    * <p>
220    * Modifying this Map (or any item in it) will affect the real schema.  However if you 
221    * make any modifications, be sure to call {@link IndexSchema#refreshAnalyzers()} to
222    * update the Analyzers for the registered fields.
223    * </p>
224    * 
225    * <p>
226    * NOTE: this function is not thread safe.  However, it is safe to use within the standard
227    * <code>inform( SolrCore core )</code> function for <code>SolrCoreAware</code> classes.
228    * Outside <code>inform</code>, this could potentially throw a ConcurrentModificationException
229    * </p>
230    */
231   public Map<String,FieldType> getFieldTypes() { return fieldTypes; }
232 
233   /**
234    * Provides direct access to the List containing all fields with a default value
235    */
236   public List<SchemaField> getFieldsWithDefaultValue() { return fieldsWithDefaultValue; }
237 
238   /**
239    * Provides direct access to the List containing all required fields.  This
240    * list contains all fields with default values.
241    */
242   public Collection<SchemaField> getRequiredFields() { return requiredFields; }
243 
244   protected Similarity similarity;
245 
246   /**
247    * Returns the Similarity used for this index
248    */
249   public Similarity getSimilarity() {
250     if (null == similarity) {
251       similarity = similarityFactory.getSimilarity();
252     }
253     return similarity; 
254   }
255 
256   protected SimilarityFactory similarityFactory;
257   protected boolean isExplicitSimilarity = false;
258 
259 
260   /** Returns the SimilarityFactory that constructed the Similarity for this index */
261   public SimilarityFactory getSimilarityFactory() { return similarityFactory; }
262   
263   /**
264    * Returns the Analyzer used when indexing documents for this index
265    *
266    * <p>
267    * This Analyzer is field (and dynamic field) name aware, and delegates to
268    * a field specific Analyzer based on the field type.
269    * </p>
270    */
271   public Analyzer getIndexAnalyzer() { return indexAnalyzer; }
272 
273   /**
274    * Returns the Analyzer used when searching this index
275    *
276    * <p>
277    * This Analyzer is field (and dynamic field) name aware, and delegates to
278    * a field specific Analyzer based on the field type.
279    * </p>
280    */
281   public Analyzer getQueryAnalyzer() { return queryAnalyzer; }
282 
283   
284   /**
285    * Name of the default search field specified in the schema file.
286    * <br><b>Note:</b>Avoid calling this, try to use this method so that the 'df' param is consulted as an override:
287    * {@link org.apache.solr.search.QueryParsing#getDefaultField(IndexSchema, String)}
288    */
289   public String getDefaultSearchFieldName() {
290     return defaultSearchFieldName;
291   }
292 
293   /**
294    * default operator ("AND" or "OR") for QueryParser
295    */
296   public String getQueryParserDefaultOperator() {
297     return queryParserDefaultOperator;
298   }
299 
300   protected SchemaField uniqueKeyField;
301 
302   /**
303    * Unique Key field specified in the schema file
304    * @return null if this schema has no unique key field
305    */
306   public SchemaField getUniqueKeyField() { return uniqueKeyField; }
307 
308   protected String uniqueKeyFieldName;
309   protected FieldType uniqueKeyFieldType;
310 
311   /**
312    * The raw (field type encoded) value of the Unique Key field for
313    * the specified Document
314    * @return null if this schema has no unique key field
315    * @see #printableUniqueKey
316    */
317   public IndexableField getUniqueKeyField(org.apache.lucene.document.Document doc) {
318     return doc.getField(uniqueKeyFieldName);  // this should return null if name is null
319   }
320 
321   /**
322    * The printable value of the Unique Key field for
323    * the specified Document
324    * @return null if this schema has no unique key field
325    */
326   public String printableUniqueKey(org.apache.lucene.document.Document doc) {
327     IndexableField f = doc.getField(uniqueKeyFieldName);
328     return f==null ? null : uniqueKeyFieldType.toExternal(f);
329   }
330 
331   private SchemaField getIndexedField(String fname) {
332     SchemaField f = getFields().get(fname);
333     if (f==null) {
334       throw new RuntimeException("unknown field '" + fname + "'");
335     }
336     if (!f.indexed()) {
337       throw new RuntimeException("'"+fname+"' is not an indexed field:" + f);
338     }
339     return f;
340   }
341   
342   /**
343    * This will re-create the Analyzers.  If you make any modifications to
344    * the Field map ({@link IndexSchema#getFields()}, this function is required
345    * to synch the internally cached field analyzers.
346    * 
347    * @since solr 1.3
348    */
349   public void refreshAnalyzers() {
350     indexAnalyzer = new SolrIndexAnalyzer();
351     queryAnalyzer = new SolrQueryAnalyzer();
352   }
353   
354   public Map<String,UninvertingReader.Type> getUninversionMap(IndexReader reader) {
355     Map<String,UninvertingReader.Type> map = new HashMap<>();
356     for (FieldInfo f : MultiFields.getMergedFieldInfos(reader)) {
357       if (f.getDocValuesType() == DocValuesType.NONE && f.getIndexOptions() != IndexOptions.NONE) {
358         SchemaField sf = getFieldOrNull(f.name);
359         if (sf != null) {
360           UninvertingReader.Type type = sf.getType().getUninversionType(sf);
361           if (type != null) {
362             map.put(f.name, type);
363           }
364         }
365       }
366     }
367     return map;
368   }
369 
370   /**
371    * Writes the schema in schema.xml format to the given writer 
372    */
373   void persist(Writer writer) throws IOException {
374     final SolrQueryResponse response = new SolrQueryResponse();
375     response.add(IndexSchema.SCHEMA, getNamedPropertyValues());
376     final NamedList args = new NamedList(Arrays.<Object>asList("indent", "on"));
377     final LocalSolrQueryRequest req = new LocalSolrQueryRequest(null, args);
378     final SchemaXmlWriter schemaXmlWriter = new SchemaXmlWriter(writer, req, response);
379     schemaXmlWriter.setEmitManagedSchemaDoNotEditWarning(true);
380     schemaXmlWriter.writeResponse();
381     schemaXmlWriter.close();
382   }
383 
384   public boolean isMutable() {
385     return false;
386   }
387 
388   private class SolrIndexAnalyzer extends DelegatingAnalyzerWrapper {
389     protected final HashMap<String, Analyzer> analyzers;
390 
391     SolrIndexAnalyzer() {
392       super(PER_FIELD_REUSE_STRATEGY);
393       analyzers = analyzerCache();
394     }
395 
396     protected HashMap<String, Analyzer> analyzerCache() {
397       HashMap<String, Analyzer> cache = new HashMap<>();
398       for (SchemaField f : getFields().values()) {
399         Analyzer analyzer = f.getType().getIndexAnalyzer();
400         cache.put(f.getName(), analyzer);
401       }
402       return cache;
403     }
404 
405     @Override
406     protected Analyzer getWrappedAnalyzer(String fieldName) {
407       Analyzer analyzer = analyzers.get(fieldName);
408       return analyzer != null ? analyzer : getDynamicFieldType(fieldName).getIndexAnalyzer();
409     }
410 
411   }
412 
413   private class SolrQueryAnalyzer extends SolrIndexAnalyzer {
414     SolrQueryAnalyzer() {}
415 
416     @Override
417     protected HashMap<String, Analyzer> analyzerCache() {
418       HashMap<String, Analyzer> cache = new HashMap<>();
419        for (SchemaField f : getFields().values()) {
420         Analyzer analyzer = f.getType().getQueryAnalyzer();
421         cache.put(f.getName(), analyzer);
422       }
423       return cache;
424     }
425 
426     @Override
427     protected Analyzer getWrappedAnalyzer(String fieldName) {
428       Analyzer analyzer = analyzers.get(fieldName);
429       return analyzer != null ? analyzer : getDynamicFieldType(fieldName).getQueryAnalyzer();
430     }
431   }
432 
433   protected void readSchema(InputSource is) {
434     try {
435       // pass the config resource loader to avoid building an empty one for no reason:
436       // in the current case though, the stream is valid so we wont load the resource by name
437       Config schemaConf = new Config(loader, SCHEMA, is, SLASH+SCHEMA+SLASH);
438       Document document = schemaConf.getDocument();
439       final XPath xpath = schemaConf.getXPath();
440       String expression = stepsToPath(SCHEMA, AT + NAME);
441       Node nd = (Node) xpath.evaluate(expression, document, XPathConstants.NODE);
442       StringBuilder sb = new StringBuilder();
443       // Another case where the initialization from the test harness is different than the "real world"
444       sb.append("[");
445       if (loader.getCoreProperties() != null) {
446         sb.append(loader.getCoreProperties().getProperty(SOLR_CORE_NAME));
447       } else {
448         sb.append("null");
449       }
450       sb.append("] ");
451       if (nd==null) {
452         sb.append("schema has no name!");
453         log.warn(sb.toString());
454       } else {
455         name = nd.getNodeValue();
456         sb.append("Schema ");
457         sb.append(NAME);
458         sb.append("=");
459         sb.append(name);
460         log.info(sb.toString());
461       }
462 
463       //                      /schema/@version
464       expression = stepsToPath(SCHEMA, AT + VERSION);
465       version = schemaConf.getFloat(expression, 1.0f);
466 
467       // load the Field Types
468       final FieldTypePluginLoader typeLoader = new FieldTypePluginLoader(this, fieldTypes, schemaAware);
469       expression = getFieldTypeXPathExpressions();
470       NodeList nodes = (NodeList) xpath.evaluate(expression, document, XPathConstants.NODESET);
471       typeLoader.load(loader, nodes);
472 
473       // load the fields
474       Map<String,Boolean> explicitRequiredProp = loadFields(document, xpath);
475 
476       expression = stepsToPath(SCHEMA, SIMILARITY); //   /schema/similarity
477       Node node = (Node) xpath.evaluate(expression, document, XPathConstants.NODE);
478       similarityFactory = readSimilarity(loader, node);
479       if (similarityFactory == null) {
480         similarityFactory = new ClassicSimilarityFactory();
481         final NamedList similarityParams = new NamedList();
482         Version luceneVersion = getDefaultLuceneMatchVersion();
483         if (!luceneVersion.onOrAfter(Version.LUCENE_4_7_0)) {
484           similarityParams.add(ClassicSimilarityFactory.DISCOUNT_OVERLAPS, false);
485         }
486         similarityFactory.init(SolrParams.toSolrParams(similarityParams));
487       } else {
488         isExplicitSimilarity = true;
489       }
490       if ( ! (similarityFactory instanceof SolrCoreAware)) {
491         // if the sim factory isn't SolrCoreAware (and hence schema aware), 
492         // then we are responsible for erroring if a field type is trying to specify a sim.
493         for (FieldType ft : fieldTypes.values()) {
494           if (null != ft.getSimilarity()) {
495             String msg = "FieldType '" + ft.getTypeName()
496                 + "' is configured with a similarity, but the global similarity does not support it: " 
497                 + similarityFactory.getClass();
498             log.error(msg);
499             throw new SolrException(ErrorCode.SERVER_ERROR, msg);
500           }
501         }
502       }
503 
504       //                      /schema/defaultSearchField/text()
505       expression = stepsToPath(SCHEMA, DEFAULT_SEARCH_FIELD, TEXT_FUNCTION);
506       node = (Node) xpath.evaluate(expression, document, XPathConstants.NODE);
507       if (node==null) {
508         log.debug("no default search field specified in schema.");
509       } else {
510         defaultSearchFieldName=node.getNodeValue().trim();
511         // throw exception if specified, but not found or not indexed
512         if (defaultSearchFieldName!=null) {
513           SchemaField defaultSearchField = getFields().get(defaultSearchFieldName);
514           if ((defaultSearchField == null) || !defaultSearchField.indexed()) {
515             String msg =  "default search field '" + defaultSearchFieldName + "' not defined or not indexed" ;
516             throw new SolrException(ErrorCode.SERVER_ERROR, msg);
517           }
518         }
519         log.info("default search field in schema is "+defaultSearchFieldName);
520       }
521 
522       //                      /schema/solrQueryParser/@defaultOperator
523       expression = stepsToPath(SCHEMA, SOLR_QUERY_PARSER, AT + DEFAULT_OPERATOR);
524       node = (Node) xpath.evaluate(expression, document, XPathConstants.NODE);
525       if (node==null) {
526         log.debug("using default query parser operator (OR)");
527       } else {
528         isExplicitQueryParserDefaultOperator = true;
529         queryParserDefaultOperator=node.getNodeValue().trim();
530         log.info("query parser default operator is "+queryParserDefaultOperator);
531       }
532 
533       //                      /schema/uniqueKey/text()
534       expression = stepsToPath(SCHEMA, UNIQUE_KEY, TEXT_FUNCTION);
535       node = (Node) xpath.evaluate(expression, document, XPathConstants.NODE);
536       if (node==null) {
537         log.warn("no " + UNIQUE_KEY + " specified in schema.");
538       } else {
539         uniqueKeyField=getIndexedField(node.getNodeValue().trim());
540         if (null != uniqueKeyField.getDefaultValue()) {
541           String msg = UNIQUE_KEY + " field ("+uniqueKeyFieldName+
542               ") can not be configured with a default value ("+
543               uniqueKeyField.getDefaultValue()+")";
544           log.error(msg);
545           throw new SolrException(ErrorCode.SERVER_ERROR, msg);
546         }
547 
548         if (!uniqueKeyField.stored()) {
549           log.warn(UNIQUE_KEY + " is not stored - distributed search and MoreLikeThis will not work");
550         }
551         if (uniqueKeyField.multiValued()) {
552           String msg = UNIQUE_KEY + " field ("+uniqueKeyFieldName+
553               ") can not be configured to be multivalued";
554           log.error(msg);
555           throw new SolrException(ErrorCode.SERVER_ERROR, msg);
556         }
557         uniqueKeyFieldName=uniqueKeyField.getName();
558         uniqueKeyFieldType=uniqueKeyField.getType();
559         log.info("unique key field: "+uniqueKeyFieldName);
560       
561         // Unless the uniqueKeyField is marked 'required=false' then make sure it exists
562         if( Boolean.FALSE != explicitRequiredProp.get( uniqueKeyFieldName ) ) {
563           uniqueKeyField.required = true;
564           requiredFields.add(uniqueKeyField);
565         }
566       }                
567 
568       /////////////// parse out copyField commands ///////////////
569       // Map<String,ArrayList<SchemaField>> cfields = new HashMap<String,ArrayList<SchemaField>>();
570       // expression = "/schema/copyField";
571     
572       dynamicCopyFields = new DynamicCopy[] {};
573       loadCopyFields(document, xpath);
574 
575       postReadInform();
576 
577     } catch (SolrException e) {
578       throw new SolrException(ErrorCode.getErrorCode(e.code()),
579           "Can't load schema " + loader.resourceLocation(resourceName) + ": " + e.getMessage(), e);
580     } catch(Exception e) {
581       // unexpected exception...
582       throw new SolrException(ErrorCode.SERVER_ERROR,
583           "Can't load schema " + loader.resourceLocation(resourceName) + ": " + e.getMessage(), e);
584     }
585 
586     // create the field analyzers
587     refreshAnalyzers();
588   }
589   
590   protected void postReadInform() {
591     //Run the callbacks on SchemaAware now that everything else is done
592     for (SchemaAware aware : schemaAware) {
593       aware.inform(this);
594     }
595   }
596 
597   /** 
598    * Loads fields and dynamic fields.
599    * 
600    * @return a map from field name to explicit required value  
601    */ 
602   protected synchronized Map<String,Boolean> loadFields(Document document, XPath xpath) throws XPathExpressionException {
603     // Hang on to the fields that say if they are required -- this lets us set a reasonable default for the unique key
604     Map<String,Boolean> explicitRequiredProp = new HashMap<>();
605     
606     ArrayList<DynamicField> dFields = new ArrayList<>();
607 
608     //                  /schema/field | /schema/dynamicField | /schema/fields/field | /schema/fields/dynamicField
609     String expression = stepsToPath(SCHEMA, FIELD)
610         + XPATH_OR + stepsToPath(SCHEMA, DYNAMIC_FIELD)
611         + XPATH_OR + stepsToPath(SCHEMA, FIELDS, FIELD)
612         + XPATH_OR + stepsToPath(SCHEMA, FIELDS, DYNAMIC_FIELD);
613 
614     NodeList nodes = (NodeList)xpath.evaluate(expression, document, XPathConstants.NODESET);
615 
616     for (int i=0; i<nodes.getLength(); i++) {
617       Node node = nodes.item(i);
618 
619       NamedNodeMap attrs = node.getAttributes();
620 
621       String name = DOMUtil.getAttr(attrs, NAME, "field definition");
622       log.trace("reading field def "+name);
623       String type = DOMUtil.getAttr(attrs, TYPE, "field " + name);
624 
625       FieldType ft = fieldTypes.get(type);
626       if (ft==null) {
627         throw new SolrException
628             (ErrorCode.BAD_REQUEST, "Unknown " + FIELD_TYPE + " '" + type + "' specified on field " + name);
629       }
630 
631       Map<String,String> args = DOMUtil.toMapExcept(attrs, NAME, TYPE);
632       if (null != args.get(REQUIRED)) {
633         explicitRequiredProp.put(name, Boolean.valueOf(args.get(REQUIRED)));
634       }
635 
636       SchemaField f = SchemaField.create(name,ft,args);
637 
638       if (node.getNodeName().equals(FIELD)) {
639         SchemaField old = fields.put(f.getName(),f);
640         if( old != null ) {
641           String msg = "[schema.xml] Duplicate field definition for '"
642             + f.getName() + "' [[["+old.toString()+"]]] and [[["+f.toString()+"]]]";
643           throw new SolrException(ErrorCode.SERVER_ERROR, msg );
644         }
645         log.debug("field defined: " + f);
646         if( f.getDefaultValue() != null ) {
647           log.debug(name+" contains default value: " + f.getDefaultValue());
648           fieldsWithDefaultValue.add( f );
649         }
650         if (f.isRequired()) {
651           log.debug(name+" is required in this schema");
652           requiredFields.add(f);
653         }
654       } else if (node.getNodeName().equals(DYNAMIC_FIELD)) {
655         if (isValidDynamicField(dFields, f)) {
656           addDynamicFieldNoDupCheck(dFields, f);
657         }
658       } else {
659         // we should never get here
660         throw new RuntimeException("Unknown field type");
661       }
662     }
663 
664     //fields with default values are by definition required
665     //add them to required fields, and we only have to loop once
666     // in DocumentBuilder.getDoc()
667     requiredFields.addAll(fieldsWithDefaultValue);
668 
669     dynamicFields = dynamicFieldListToSortedArray(dFields);
670                                                                    
671     return explicitRequiredProp;
672   }
673   
674   /**
675    * Sort the dynamic fields and stuff them in a normal array for faster access.
676    */
677   protected static DynamicField[] dynamicFieldListToSortedArray(List<DynamicField> dynamicFieldList) {
678     // Avoid creating the array twice by converting to an array first and using Arrays.sort(),
679     // rather than Collections.sort() then converting to an array, since Collections.sort()
680     // copies to an array first, then sets each collection member from the array. 
681     DynamicField[] dFields = dynamicFieldList.toArray(new DynamicField[dynamicFieldList.size()]);
682     Arrays.sort(dFields);
683 
684     log.trace("Dynamic Field Ordering:" + Arrays.toString(dFields));
685 
686     return dFields; 
687   }
688 
689   /**
690    * Loads the copy fields
691    */
692   protected synchronized void loadCopyFields(Document document, XPath xpath) throws XPathExpressionException {
693     String expression = "//" + COPY_FIELD;
694     NodeList nodes = (NodeList)xpath.evaluate(expression, document, XPathConstants.NODESET);
695 
696     for (int i=0; i<nodes.getLength(); i++) {
697       Node node = nodes.item(i);
698       NamedNodeMap attrs = node.getAttributes();
699 
700       String source = DOMUtil.getAttr(attrs, SOURCE, COPY_FIELD + " definition");
701       String dest   = DOMUtil.getAttr(attrs, DESTINATION,  COPY_FIELD + " definition");
702       String maxChars = DOMUtil.getAttr(attrs, MAX_CHARS);
703 
704       int maxCharsInt = CopyField.UNLIMITED;
705       if (maxChars != null) {
706         try {
707           maxCharsInt = Integer.parseInt(maxChars);
708         } catch (NumberFormatException e) {
709           log.warn("Couldn't parse " + MAX_CHARS + " attribute for " + COPY_FIELD + " from "
710                   + source + " to " + dest + " as integer. The whole field will be copied.");
711         }
712       }
713 
714       if (dest.equals(uniqueKeyFieldName)) {
715         String msg = UNIQUE_KEY + " field ("+uniqueKeyFieldName+
716           ") can not be the " + DESTINATION + " of a " + COPY_FIELD + "(" + SOURCE + "=" +source+")";
717         log.error(msg);
718         throw new SolrException(ErrorCode.SERVER_ERROR, msg);
719       }
720       
721       registerCopyField(source, dest, maxCharsInt);
722     }
723       
724     for (Map.Entry<SchemaField, Integer> entry : copyFieldTargetCounts.entrySet()) {
725       if (entry.getValue() > 1 && !entry.getKey().multiValued())  {
726         log.warn("Field " + entry.getKey().name + " is not multivalued "+
727             "and destination for multiple " + COPY_FIELDS + " ("+
728             entry.getValue()+")");
729       }
730     }
731   }
732 
733   /**
734    * Converts a sequence of path steps into a rooted path, by inserting slashes in front of each step.
735    * @param steps The steps to join with slashes to form a path
736    * @return a rooted path: a leading slash followed by the given steps joined with slashes
737    */
738   private String stepsToPath(String... steps) {
739     StringBuilder builder = new StringBuilder();
740     for (String step : steps) { builder.append(SLASH).append(step); }
741     return builder.toString();
742   }
743 
744   /** Returns true if the given name has exactly one asterisk either at the start or end of the name */
745   protected static boolean isValidFieldGlob(String name) {
746     if (name.startsWith("*") || name.endsWith("*")) {
747       int count = 0;
748       for (int pos = 0 ; pos < name.length() && -1 != (pos = name.indexOf('*', pos)) ; ++pos) ++count;
749       if (1 == count) return true;
750     }
751     return false;
752   }
753   
754   protected boolean isValidDynamicField(List<DynamicField> dFields, SchemaField f) {
755     String glob = f.getName();
756     if (f.getDefaultValue() != null) {
757       throw new SolrException(ErrorCode.SERVER_ERROR,
758           DYNAMIC_FIELD + " can not have a default value: " + glob);
759     }
760     if (f.isRequired()) {
761       throw new SolrException(ErrorCode.SERVER_ERROR,
762           DYNAMIC_FIELD + " can not be required: " + glob);
763     }
764     if ( ! isValidFieldGlob(glob)) {
765       String msg = "Dynamic field name '" + glob
766           + "' should have either a leading or a trailing asterisk, and no others.";
767       throw new SolrException(ErrorCode.SERVER_ERROR, msg);
768     }
769     if (isDuplicateDynField(dFields, f)) {
770       String msg = "[schema.xml] Duplicate DynamicField definition for '" + glob + "'";
771       throw new SolrException(ErrorCode.SERVER_ERROR, msg);
772     }
773     return true;
774   }
775 
776 
777   /**
778    * Register one or more new Dynamic Fields with the Schema.
779    * @param fields The sequence of {@link org.apache.solr.schema.SchemaField}
780    */
781   public void registerDynamicFields(SchemaField... fields) {
782     List<DynamicField> dynFields = new ArrayList<>(Arrays.asList(dynamicFields));
783     for (SchemaField field : fields) {
784       if (isDuplicateDynField(dynFields, field)) {
785         log.debug("dynamic field already exists: dynamic field: [" + field.getName() + "]");
786       } else {
787         log.debug("dynamic field creation for schema field: " + field.getName());
788         addDynamicFieldNoDupCheck(dynFields, field);
789       }
790     }
791     dynamicFields = dynamicFieldListToSortedArray(dynFields);
792   }
793 
794   private void addDynamicFieldNoDupCheck(List<DynamicField> dFields, SchemaField f) {
795     dFields.add(new DynamicField(f));
796     log.debug("dynamic field defined: " + f);
797   }
798 
799   protected boolean isDuplicateDynField(List<DynamicField> dFields, SchemaField f) {
800     for (DynamicField df : dFields) {
801       if (df.getRegex().equals(f.name)) return true;
802     }
803     return false;
804   }
805 
806   public void registerCopyField( String source, String dest ) {
807     registerCopyField(source, dest, CopyField.UNLIMITED);
808   }
809 
810   /**
811    * <p>
812    * NOTE: this function is not thread safe.  However, it is safe to use within the standard
813    * <code>inform( SolrCore core )</code> function for <code>SolrCoreAware</code> classes.
814    * Outside <code>inform</code>, this could potentially throw a ConcurrentModificationException
815    * </p>
816    * 
817    * @see SolrCoreAware
818    */
819   public void registerCopyField(String source, String dest, int maxChars) {
820     log.debug(COPY_FIELD + " " + SOURCE + "='" + source + "' " + DESTINATION + "='" + dest
821               + "' " + MAX_CHARS + "=" + maxChars);
822 
823     DynamicField destDynamicField = null;
824     SchemaField destSchemaField = fields.get(dest);
825     SchemaField sourceSchemaField = fields.get(source);
826     
827     DynamicField sourceDynamicBase = null;
828     DynamicField destDynamicBase = null;
829     
830     boolean sourceIsDynamicFieldReference = false;
831     boolean sourceIsExplicitFieldGlob = false;
832 
833 
834     final String invalidGlobMessage = "is an invalid glob: either it contains more than one asterisk,"
835                                     + " or the asterisk occurs neither at the start nor at the end.";
836     final boolean sourceIsGlob = isValidFieldGlob(source);
837     if (source.contains("*") && ! sourceIsGlob) {
838       String msg = "copyField source :'" + source + "' " + invalidGlobMessage;
839       throw new SolrException(ErrorCode.SERVER_ERROR, msg);
840     }
841     if (dest.contains("*") && ! isValidFieldGlob(dest)) {
842       String msg = "copyField dest :'" + dest + "' " + invalidGlobMessage;
843       throw new SolrException(ErrorCode.SERVER_ERROR, msg);
844     }
845 
846     if (null == sourceSchemaField && sourceIsGlob) {
847       Pattern pattern = Pattern.compile(source.replace("*", ".*")); // glob->regex
848       for (String field : fields.keySet()) {
849         if (pattern.matcher(field).matches()) {
850           sourceIsExplicitFieldGlob = true;
851           break;
852         }
853       }
854     }
855     
856     if (null == destSchemaField || (null == sourceSchemaField && ! sourceIsExplicitFieldGlob)) {
857       // Go through dynamicFields array only once, collecting info for both source and dest fields, if needed
858       for (DynamicField dynamicField : dynamicFields) {
859         if (null == sourceSchemaField && ! sourceIsDynamicFieldReference && ! sourceIsExplicitFieldGlob) {
860           if (dynamicField.matches(source)) {
861             sourceIsDynamicFieldReference = true;
862             if ( ! source.equals(dynamicField.getRegex())) {
863               sourceDynamicBase = dynamicField;
864             }
865           }
866         }
867         if (null == destSchemaField) {
868           if (dest.equals(dynamicField.getRegex())) {
869             destDynamicField = dynamicField;
870             destSchemaField = dynamicField.prototype;
871           } else if (dynamicField.matches(dest)) {
872             destSchemaField = dynamicField.makeSchemaField(dest);
873             destDynamicField = new DynamicField(destSchemaField);
874             destDynamicBase = dynamicField;
875           }
876         }
877         if (null != destSchemaField 
878             && (null != sourceSchemaField || sourceIsDynamicFieldReference || sourceIsExplicitFieldGlob)) {
879           break;
880         }
881       }
882     }
883     if (null == sourceSchemaField && ! sourceIsGlob && ! sourceIsDynamicFieldReference) {
884       String msg = "copyField source :'" + source + "' is not a glob and doesn't match any explicit field or dynamicField.";
885       throw new SolrException(ErrorCode.SERVER_ERROR, msg);
886     }
887     if (null == destSchemaField) {
888       String msg = "copyField dest :'" + dest + "' is not an explicit field and doesn't match a dynamicField.";
889       throw new SolrException(ErrorCode.SERVER_ERROR, msg);
890     }
891     if (sourceIsGlob) {
892       if (null != destDynamicField) { // source: glob ; dest: dynamic field ref
893         registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
894         incrementCopyFieldTargetCount(destSchemaField);
895       } else {                        // source: glob ; dest: explicit field
896         destDynamicField = new DynamicField(destSchemaField);
897         registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, null));
898         incrementCopyFieldTargetCount(destSchemaField);
899       }
900     } else if (sourceIsDynamicFieldReference) {
901         if (null != destDynamicField) {  // source: no-asterisk dynamic field ref ; dest: dynamic field ref
902           registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
903           incrementCopyFieldTargetCount(destSchemaField);
904         } else {                        // source: no-asterisk dynamic field ref ; dest: explicit field
905           sourceSchemaField = getField(source);
906           registerExplicitSrcAndDestFields(source, maxChars, destSchemaField, sourceSchemaField);
907         }
908     } else {
909       if (null != destDynamicField) { // source: explicit field ; dest: dynamic field reference
910         if (destDynamicField.pattern instanceof DynamicReplacement.DynamicPattern.NameEquals) {
911           // Dynamic dest with no asterisk is acceptable
912           registerDynamicCopyField(new DynamicCopy(source, destDynamicField, maxChars, sourceDynamicBase, destDynamicBase));
913           incrementCopyFieldTargetCount(destSchemaField);
914         } else {                    // source: explicit field ; dest: dynamic field with an asterisk
915           String msg = "copyField only supports a dynamic destination with an asterisk "
916                      + "if the source also has an asterisk";
917           throw new SolrException(ErrorCode.SERVER_ERROR, msg);
918         }
919       } else {                        // source & dest: explicit fields
920         registerExplicitSrcAndDestFields(source, maxChars, destSchemaField, sourceSchemaField);
921       }
922     }
923   }
924 
925   protected void registerExplicitSrcAndDestFields(String source, int maxChars, SchemaField destSchemaField, SchemaField sourceSchemaField) {
926     List<CopyField> copyFieldList = copyFieldsMap.get(source);
927     if (copyFieldList == null) {
928       copyFieldList = new ArrayList<>();
929       copyFieldsMap.put(source, copyFieldList);
930     }
931     copyFieldList.add(new CopyField(sourceSchemaField, destSchemaField, maxChars));
932     incrementCopyFieldTargetCount(destSchemaField);
933   }
934   
935   private void incrementCopyFieldTargetCount(SchemaField dest) {
936     copyFieldTargetCounts.put(dest, copyFieldTargetCounts.containsKey(dest) ? copyFieldTargetCounts.get(dest) + 1 : 1);
937   }
938   
939   private void registerDynamicCopyField( DynamicCopy dcopy ) {
940     if( dynamicCopyFields == null ) {
941       dynamicCopyFields = new DynamicCopy[] {dcopy};
942     }
943     else {
944       DynamicCopy[] temp = new DynamicCopy[dynamicCopyFields.length+1];
945       System.arraycopy(dynamicCopyFields,0,temp,0,dynamicCopyFields.length);
946       temp[temp.length -1] = dcopy;
947       dynamicCopyFields = temp;
948     }
949     log.trace("Dynamic Copy Field:" + dcopy);
950   }
951 
952   static SimilarityFactory readSimilarity(SolrResourceLoader loader, Node node) {
953     if (node==null) {
954       return null;
955     } else {
956       SimilarityFactory similarityFactory;
957       final String classArg = ((Element) node).getAttribute(SimilarityFactory.CLASS_NAME);
958       final Object obj = loader.newInstance(classArg, Object.class, "search.similarities.");
959       if (obj instanceof SimilarityFactory) {
960         // configure a factory, get a similarity back
961         final NamedList<Object> namedList = DOMUtil.childNodesToNamedList(node);
962         namedList.add(SimilarityFactory.CLASS_NAME, classArg);
963         SolrParams params = SolrParams.toSolrParams(namedList);
964         similarityFactory = (SimilarityFactory)obj;
965         similarityFactory.init(params);
966       } else {
967         // just like always, assume it's a Similarity and get a ClassCastException - reasonable error handling
968         similarityFactory = new SimilarityFactory() {
969           @Override
970           public Similarity getSimilarity() {
971             return (Similarity) obj;
972           }
973         };
974       }
975       return similarityFactory;
976     }
977   }
978 
979 
980   public static abstract class DynamicReplacement implements Comparable<DynamicReplacement> {
981     abstract protected static class DynamicPattern {
982       protected final String regex;
983       protected final String fixedStr;
984 
985       protected DynamicPattern(String regex, String fixedStr) { this.regex = regex; this.fixedStr = fixedStr; }
986 
987       static DynamicPattern createPattern(String regex) {
988         if (regex.startsWith("*")) { return new NameEndsWith(regex); }
989         else if (regex.endsWith("*")) { return new NameStartsWith(regex); }
990         else { return new NameEquals(regex);
991         }
992       }
993       
994       /** Returns true if the given name matches this pattern */
995       abstract boolean matches(String name);
996 
997       /** Returns the remainder of the given name after removing this pattern's fixed string component */
998       abstract String remainder(String name);
999 
1000       /** Returns the result of combining this pattern's fixed string component with the given replacement */
1001       abstract String subst(String replacement);
1002       
1003       /** Returns the length of the original regex, including the asterisk, if any. */
1004       public int length() { return regex.length(); }
1005 
1006       private static class NameStartsWith extends DynamicPattern {
1007         NameStartsWith(String regex) { super(regex, regex.substring(0, regex.length() - 1)); }
1008         boolean matches(String name) { return name.startsWith(fixedStr); }
1009         String remainder(String name) { return name.substring(fixedStr.length()); }
1010         String subst(String replacement) { return fixedStr + replacement; }
1011       }
1012       private static class NameEndsWith extends DynamicPattern {
1013         NameEndsWith(String regex) { super(regex, regex.substring(1)); }
1014         boolean matches(String name) { return name.endsWith(fixedStr); }
1015         String remainder(String name) { return name.substring(0, name.length() - fixedStr.length()); }
1016         String subst(String replacement) { return replacement + fixedStr; }
1017       }
1018       private static class NameEquals extends DynamicPattern {
1019         NameEquals(String regex) { super(regex, regex); }
1020         boolean matches(String name) { return regex.equals(name); }
1021         String remainder(String name) { return ""; }
1022         String subst(String replacement) { return fixedStr; }
1023       }
1024     }
1025 
1026     protected DynamicPattern pattern;
1027 
1028     public boolean matches(String name) { return pattern.matches(name); }
1029 
1030     protected DynamicReplacement(String regex) {
1031       pattern = DynamicPattern.createPattern(regex);
1032     }
1033 
1034     /**
1035      * Sort order is based on length of regex.  Longest comes first.
1036      * @param other The object to compare to.
1037      * @return a negative integer, zero, or a positive integer
1038      * as this object is less than, equal to, or greater than
1039      * the specified object.
1040      */
1041     @Override
1042     public int compareTo(DynamicReplacement other) {
1043       return other.pattern.length() - pattern.length();
1044     }
1045     
1046     /** Returns the regex used to create this instance's pattern */
1047     public String getRegex() {
1048       return pattern.regex;
1049     }
1050   }
1051 
1052 
1053   public final static class DynamicField extends DynamicReplacement {
1054     private final SchemaField prototype;
1055     public SchemaField getPrototype() { return prototype; }
1056 
1057     DynamicField(SchemaField prototype) {
1058       super(prototype.name);
1059       this.prototype=prototype;
1060     }
1061 
1062     SchemaField makeSchemaField(String name) {
1063       // could have a cache instead of returning a new one each time, but it might
1064       // not be worth it.
1065       // Actually, a higher level cache could be worth it to avoid too many
1066       // .startsWith() and .endsWith() comparisons.  it depends on how many
1067       // dynamic fields there are.
1068       return new SchemaField(prototype, name);
1069     }
1070 
1071     @Override
1072     public String toString() {
1073       return prototype.toString();
1074     }
1075   }
1076 
1077   public static class DynamicCopy extends DynamicReplacement {
1078     private final DynamicField destination;
1079     
1080     private final int maxChars;
1081     public int getMaxChars() { return maxChars; }
1082 
1083     final DynamicField sourceDynamicBase;
1084     public DynamicField getSourceDynamicBase() { return sourceDynamicBase; }
1085 
1086     final DynamicField destDynamicBase;
1087     public DynamicField getDestDynamicBase() { return destDynamicBase; }
1088 
1089     DynamicCopy(String sourceRegex, DynamicField destination, int maxChars, 
1090                 DynamicField sourceDynamicBase, DynamicField destDynamicBase) {
1091       super(sourceRegex);
1092       this.destination = destination;
1093       this.maxChars = maxChars;
1094       this.sourceDynamicBase = sourceDynamicBase;
1095       this.destDynamicBase = destDynamicBase;
1096     }
1097 
1098     public DynamicField getDestination() { return destination; }
1099 
1100     public String getDestFieldName() { return destination.getRegex(); }
1101 
1102     /**
1103      *  Generates a destination field name based on this source pattern,
1104      *  by substituting the remainder of this source pattern into the
1105      *  the given destination pattern.
1106      */
1107     public SchemaField getTargetField(String sourceField) {
1108       String remainder = pattern.remainder(sourceField);
1109       String targetFieldName = destination.pattern.subst(remainder);
1110       return destination.makeSchemaField(targetFieldName);
1111     }
1112 
1113     
1114     @Override
1115     public String toString() {
1116       return destination.prototype.toString();
1117     }
1118   }
1119 
1120   public SchemaField[] getDynamicFieldPrototypes() {
1121     SchemaField[] df = new SchemaField[dynamicFields.length];
1122     for (int i=0;i<dynamicFields.length;i++) {
1123       df[i] = dynamicFields[i].prototype;
1124     }
1125     return df;
1126   }
1127 
1128   public String getDynamicPattern(String fieldName) {
1129    for (DynamicField df : dynamicFields) {
1130      if (df.matches(fieldName)) return df.getRegex();
1131    }
1132    return  null; 
1133   }
1134   
1135   /**
1136    * Does the schema explicitly define the specified field, i.e. not as a result
1137    * of a copyField declaration?  We consider it explicitly defined if it matches
1138    * a field name or a dynamicField name.
1139    * @return true if explicitly declared in the schema.
1140    */
1141   public boolean hasExplicitField(String fieldName) {
1142     if (fields.containsKey(fieldName)) {
1143       return true;
1144     }
1145 
1146     for (DynamicField df : dynamicFields) {
1147       if (fieldName.equals(df.getRegex())) return true;
1148     }
1149 
1150     return false;
1151   }
1152 
1153   /**
1154    * Is the specified field dynamic or not.
1155    * @return true if the specified field is dynamic
1156    */
1157   public boolean isDynamicField(String fieldName) {
1158     if(fields.containsKey(fieldName)) {
1159       return false;
1160     }
1161 
1162     for (DynamicField df : dynamicFields) {
1163       if (df.matches(fieldName)) return true;
1164     }
1165 
1166     return false;
1167   }   
1168 
1169   /**
1170    * Returns the SchemaField that should be used for the specified field name, or
1171    * null if none exists.
1172    *
1173    * @param fieldName may be an explicitly defined field or a name that
1174    * matches a dynamic field.
1175    * @see #getFieldType
1176    * @see #getField(String)
1177    * @return The {@link org.apache.solr.schema.SchemaField}
1178    */
1179   public SchemaField getFieldOrNull(String fieldName) {
1180     SchemaField f = fields.get(fieldName);
1181     if (f != null) return f;
1182 
1183     for (DynamicField df : dynamicFields) {
1184       if (df.matches(fieldName)) return df.makeSchemaField(fieldName);
1185     }
1186 
1187     return f;
1188   }
1189 
1190   /**
1191    * Returns the SchemaField that should be used for the specified field name
1192    *
1193    * @param fieldName may be an explicitly defined field or a name that
1194    * matches a dynamic field.
1195    * @throws SolrException if no such field exists
1196    * @see #getFieldType
1197    * @see #getFieldOrNull(String)
1198    * @return The {@link SchemaField}
1199    */
1200   public SchemaField getField(String fieldName) {
1201     SchemaField f = getFieldOrNull(fieldName);
1202     if (f != null) return f;
1203 
1204 
1205     // Hmmm, default field could also be implemented with a dynamic field of "*".
1206     // It would have to be special-cased and only used if nothing else matched.
1207     /***  REMOVED -YCS
1208     if (defaultFieldType != null) return new SchemaField(fieldName,defaultFieldType);
1209     ***/
1210     throw new SolrException(ErrorCode.BAD_REQUEST,"undefined field: \""+fieldName+"\"");
1211   }
1212 
1213   /**
1214    * Returns the FieldType for the specified field name.
1215    *
1216    * <p>
1217    * This method exists because it can be more efficient then
1218    * {@link #getField} for dynamic fields if a full SchemaField isn't needed.
1219    * </p>
1220    *
1221    * @param fieldName may be an explicitly created field, or a name that
1222    *  excercises a dynamic field.
1223    * @throws SolrException if no such field exists
1224    * @see #getField(String)
1225    * @see #getFieldTypeNoEx
1226    */
1227   public FieldType getFieldType(String fieldName) {
1228     SchemaField f = fields.get(fieldName);
1229     if (f != null) return f.getType();
1230 
1231     return getDynamicFieldType(fieldName);
1232   }
1233 
1234   /**
1235    * Given the name of a {@link org.apache.solr.schema.FieldType} (not to be confused with {@link #getFieldType(String)} which
1236    * takes in the name of a field), return the {@link org.apache.solr.schema.FieldType}.
1237    * @param fieldTypeName The name of the {@link org.apache.solr.schema.FieldType}
1238    * @return The {@link org.apache.solr.schema.FieldType} or null.
1239    */
1240   public FieldType getFieldTypeByName(String fieldTypeName){
1241     return fieldTypes.get(fieldTypeName);
1242   }
1243 
1244   /**
1245    * Returns the FieldType for the specified field name.
1246    *
1247    * <p>
1248    * This method exists because it can be more efficient then
1249    * {@link #getField} for dynamic fields if a full SchemaField isn't needed.
1250    * </p>
1251    *
1252    * @param fieldName may be an explicitly created field, or a name that
1253    * exercises a dynamic field.
1254    * @return null if field is not defined.
1255    * @see #getField(String)
1256    * @see #getFieldTypeNoEx
1257    */
1258   public FieldType getFieldTypeNoEx(String fieldName) {
1259     SchemaField f = fields.get(fieldName);
1260     if (f != null) return f.getType();
1261     return dynFieldType(fieldName);
1262   }
1263 
1264 
1265   /**
1266    * Returns the FieldType of the best matching dynamic field for
1267    * the specified field name
1268    *
1269    * @param fieldName may be an explicitly created field, or a name that
1270    * exercises a dynamic field.
1271    * @throws SolrException if no such field exists
1272    * @see #getField(String)
1273    * @see #getFieldTypeNoEx
1274    */
1275   public FieldType getDynamicFieldType(String fieldName) {
1276      for (DynamicField df : dynamicFields) {
1277       if (df.matches(fieldName)) return df.prototype.getType();
1278     }
1279     throw new SolrException(ErrorCode.BAD_REQUEST,"undefined field "+fieldName);
1280   }
1281 
1282   private FieldType dynFieldType(String fieldName) {
1283      for (DynamicField df : dynamicFields) {
1284       if (df.matches(fieldName)) return df.prototype.getType();
1285     }
1286     return null;
1287   }
1288 
1289 
1290   /**
1291    * Get all copy fields, both the static and the dynamic ones.
1292    * @return Array of fields copied into this field
1293    */
1294 
1295   public List<String> getCopySources(String destField) {
1296     SchemaField f = getField(destField);
1297     if (!isCopyFieldTarget(f)) {
1298       return Collections.emptyList();
1299     }
1300     List<String> fieldNames = new ArrayList<>();
1301     for (Map.Entry<String, List<CopyField>> cfs : copyFieldsMap.entrySet()) {
1302       for (CopyField copyField : cfs.getValue()) {
1303         if (copyField.getDestination().getName().equals(destField)) {
1304           fieldNames.add(copyField.getSource().getName());
1305         }
1306       }
1307     }
1308     if (null != dynamicCopyFields) {
1309       for (DynamicCopy dynamicCopy : dynamicCopyFields) {
1310         if (dynamicCopy.getDestFieldName().equals(destField)) {
1311           fieldNames.add(dynamicCopy.getRegex());
1312         }
1313       }
1314     }
1315     return fieldNames;
1316   }
1317 
1318   /**
1319    * Get all copy fields for a specified source field, both static
1320    * and dynamic ones.
1321    * @return List of CopyFields to copy to.
1322    * @since solr 1.4
1323    */
1324   // This is useful when we need the maxSize param of each CopyField
1325   public List<CopyField> getCopyFieldsList(final String sourceField){
1326     final List<CopyField> result = new ArrayList<>();
1327     if (null != dynamicCopyFields) {
1328       for (DynamicCopy dynamicCopy : dynamicCopyFields) {
1329         if (dynamicCopy.matches(sourceField)) {
1330           result.add(new CopyField(getField(sourceField), dynamicCopy.getTargetField(sourceField), dynamicCopy.maxChars));
1331         }
1332       }
1333     }
1334     List<CopyField> fixedCopyFields = copyFieldsMap.get(sourceField);
1335     if (null != fixedCopyFields) {
1336       result.addAll(fixedCopyFields);
1337     }
1338 
1339     return result;
1340   }
1341   
1342   /**
1343    * Check if a field is used as the destination of a copyField operation 
1344    * 
1345    * @since solr 1.3
1346    */
1347   public boolean isCopyFieldTarget( SchemaField f ) {
1348     return copyFieldTargetCounts.containsKey( f );
1349   }
1350 
1351   /**
1352    * Get a map of property name -&gt; value for the whole schema.
1353    */
1354   public SimpleOrderedMap<Object> getNamedPropertyValues() {
1355     SimpleOrderedMap<Object> topLevel = new SimpleOrderedMap<>();
1356     topLevel.add(NAME, getSchemaName());
1357     topLevel.add(VERSION, getVersion());
1358     if (null != uniqueKeyFieldName) {
1359       topLevel.add(UNIQUE_KEY, uniqueKeyFieldName);
1360     }
1361     if (null != defaultSearchFieldName) {
1362       topLevel.add(DEFAULT_SEARCH_FIELD, defaultSearchFieldName);
1363     }
1364     if (isExplicitQueryParserDefaultOperator) {
1365       SimpleOrderedMap<Object> solrQueryParserProperties = new SimpleOrderedMap<>();
1366       solrQueryParserProperties.add(DEFAULT_OPERATOR, queryParserDefaultOperator);
1367       topLevel.add(SOLR_QUERY_PARSER, solrQueryParserProperties);
1368     }
1369     if (isExplicitSimilarity) {
1370       topLevel.add(SIMILARITY, similarityFactory.getNamedPropertyValues());
1371     }
1372     List<SimpleOrderedMap<Object>> fieldTypeProperties = new ArrayList<>();
1373     SortedMap<String,FieldType> sortedFieldTypes = new TreeMap<>(fieldTypes);
1374     for (FieldType fieldType : sortedFieldTypes.values()) {
1375       fieldTypeProperties.add(fieldType.getNamedPropertyValues(false));
1376     }
1377     topLevel.add(FIELD_TYPES, fieldTypeProperties);  
1378     List<SimpleOrderedMap<Object>> fieldProperties = new ArrayList<>();
1379     SortedSet<String> fieldNames = new TreeSet<>(fields.keySet());
1380     for (String fieldName : fieldNames) {
1381       fieldProperties.add(fields.get(fieldName).getNamedPropertyValues(false));
1382     }
1383     topLevel.add(FIELDS, fieldProperties);
1384     List<SimpleOrderedMap<Object>> dynamicFieldProperties = new ArrayList<>();
1385     for (IndexSchema.DynamicField dynamicField : dynamicFields) {
1386       if ( ! dynamicField.getRegex().startsWith(INTERNAL_POLY_FIELD_PREFIX)) { // omit internal polyfields
1387         dynamicFieldProperties.add(dynamicField.getPrototype().getNamedPropertyValues(false));
1388       }
1389     }
1390     topLevel.add(DYNAMIC_FIELDS, dynamicFieldProperties);
1391     topLevel.add(COPY_FIELDS, getCopyFieldProperties(false, null, null));
1392     return topLevel;
1393   }
1394 
1395   /**
1396    * Returns a list of copyField directives, with optional details and optionally restricting to those
1397    * directives that contain the requested source and/or destination field names.
1398    * 
1399    * @param showDetails If true, source and destination dynamic bases, and explicit fields matched by source globs,
1400    *                    will be added to dynamic copyField directives where appropriate
1401    * @param requestedSourceFields If not null, output is restricted to those copyField directives
1402    *                              with the requested source field names 
1403    * @param requestedDestinationFields If not null, output is restricted to those copyField directives
1404    *                                   with the requested destination field names 
1405    * @return a list of copyField directives 
1406    */
1407   public List<SimpleOrderedMap<Object>> getCopyFieldProperties
1408       (boolean showDetails, Set<String> requestedSourceFields, Set<String> requestedDestinationFields) {
1409     List<SimpleOrderedMap<Object>> copyFieldProperties = new ArrayList<>();
1410     SortedMap<String,List<CopyField>> sortedCopyFields = new TreeMap<>(copyFieldsMap);
1411     for (List<CopyField> copyFields : sortedCopyFields.values()) {
1412       copyFields = new ArrayList<>(copyFields);
1413       Collections.sort(copyFields, new Comparator<CopyField>() {
1414         @Override
1415         public int compare(CopyField cf1, CopyField cf2) {
1416           // sources are all the same, just sorting by destination here
1417           return cf1.getDestination().getName().compareTo(cf2.getDestination().getName());
1418         }
1419       });
1420       for (CopyField copyField : copyFields) {
1421         final String source = copyField.getSource().getName();
1422         final String destination = copyField.getDestination().getName();
1423         if (   (null == requestedSourceFields      || requestedSourceFields.contains(source))
1424             && (null == requestedDestinationFields || requestedDestinationFields.contains(destination))) {
1425           SimpleOrderedMap<Object> props = new SimpleOrderedMap<>();
1426           props.add(SOURCE, source);
1427           props.add(DESTINATION, destination);
1428             if (0 != copyField.getMaxChars()) {
1429               props.add(MAX_CHARS, copyField.getMaxChars());
1430             }
1431           copyFieldProperties.add(props);
1432         }
1433       }
1434     }
1435     if (null != dynamicCopyFields) {
1436       for (IndexSchema.DynamicCopy dynamicCopy : dynamicCopyFields) {
1437         final String source = dynamicCopy.getRegex();
1438         final String destination = dynamicCopy.getDestFieldName();
1439         if ((null == requestedSourceFields || requestedSourceFields.contains(source))
1440             && (null == requestedDestinationFields || requestedDestinationFields.contains(destination))) {
1441           SimpleOrderedMap<Object> dynamicCopyProps = new SimpleOrderedMap<>();
1442 
1443           dynamicCopyProps.add(SOURCE, dynamicCopy.getRegex());
1444           if (showDetails) {
1445             IndexSchema.DynamicField sourceDynamicBase = dynamicCopy.getSourceDynamicBase();
1446             if (null != sourceDynamicBase) {
1447               dynamicCopyProps.add(SOURCE_DYNAMIC_BASE, sourceDynamicBase.getRegex());
1448             } else if (source.contains("*")) {
1449               List<String> sourceExplicitFields = new ArrayList<>();
1450               Pattern pattern = Pattern.compile(source.replace("*", ".*"));   // glob->regex
1451               for (String field : fields.keySet()) {
1452                 if (pattern.matcher(field).matches()) {
1453                   sourceExplicitFields.add(field);
1454                 }
1455               }
1456               if (sourceExplicitFields.size() > 0) {
1457                 Collections.sort(sourceExplicitFields);
1458                 dynamicCopyProps.add(SOURCE_EXPLICIT_FIELDS, sourceExplicitFields);
1459               }
1460             }
1461           }
1462 
1463           dynamicCopyProps.add(DESTINATION, dynamicCopy.getDestFieldName());
1464           if (showDetails) {
1465             IndexSchema.DynamicField destDynamicBase = dynamicCopy.getDestDynamicBase();
1466             if (null != destDynamicBase) {
1467               dynamicCopyProps.add(DESTINATION_DYNAMIC_BASE, destDynamicBase.getRegex());
1468             }
1469           }
1470 
1471           if (0 != dynamicCopy.getMaxChars()) {
1472             dynamicCopyProps.add(MAX_CHARS, dynamicCopy.getMaxChars());
1473           }
1474 
1475           copyFieldProperties.add(dynamicCopyProps);
1476         }
1477       }
1478     }
1479     return copyFieldProperties;
1480   }
1481 
1482   protected IndexSchema(final SolrConfig solrConfig, final SolrResourceLoader loader) {
1483     this.solrConfig = solrConfig;
1484     this.loader = loader;
1485   }
1486 
1487   /**
1488    * Copies this schema, adds the given field to the copy
1489    * Requires synchronizing on the object returned by
1490    * {@link #getSchemaUpdateLock()}.
1491    *
1492    * @param newField the SchemaField to add 
1493    * @param persist to persist the schema or not
1494    * @return a new IndexSchema based on this schema with newField added
1495    * @see #newField(String, String, Map)
1496    */
1497   public IndexSchema addField(SchemaField newField, boolean persist) {
1498     return addFields(Collections.singletonList(newField), Collections.EMPTY_MAP, persist );
1499   }                                                       
1500 
1501   public IndexSchema addField(SchemaField newField) {
1502     return addField(newField, true);
1503   }
1504 
1505   /**
1506    * Copies this schema, adds the given field to the copy
1507    *  Requires synchronizing on the object returned by
1508    * {@link #getSchemaUpdateLock()}.
1509    *
1510    * @param newField the SchemaField to add
1511    * @param copyFieldNames 0 or more names of targets to copy this field to.  The targets must already exist.
1512    * @return a new IndexSchema based on this schema with newField added
1513    * @see #newField(String, String, Map)
1514    */
1515   public IndexSchema addField(SchemaField newField, Collection<String> copyFieldNames) {
1516     return addFields(singletonList(newField), singletonMap(newField.getName(), copyFieldNames), true);
1517   }
1518 
1519   /**
1520    * Copies this schema, adds the given fields to the copy
1521    * Requires synchronizing on the object returned by
1522    * {@link #getSchemaUpdateLock()}.
1523    *
1524    * @param newFields the SchemaFields to add
1525    * @return a new IndexSchema based on this schema with newFields added
1526    * @see #newField(String, String, Map)
1527    */
1528   public IndexSchema addFields(Collection<SchemaField> newFields) {
1529     return addFields(newFields, Collections.<String, Collection<String>>emptyMap(), true);
1530   }
1531 
1532   /**
1533    * Copies this schema, adds the given fields to the copy.
1534    * Requires synchronizing on the object returned by
1535    * {@link #getSchemaUpdateLock()}.
1536    *
1537    * @param newFields the SchemaFields to add
1538    * @param copyFieldNames 0 or more names of targets to copy this field to.  The target fields must already exist.
1539    * @param persist Persist the schema or not
1540    * @return a new IndexSchema based on this schema with newFields added
1541    * @see #newField(String, String, Map)
1542    */
1543   public IndexSchema addFields(Collection<SchemaField> newFields, Map<String, Collection<String>> copyFieldNames, boolean persist) {
1544     String msg = "This IndexSchema is not mutable.";
1545     log.error(msg);
1546     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1547   }
1548 
1549 
1550   /**
1551    * Copies this schema, deletes the named fields from the copy.
1552    * <p>
1553    * The schema will not be persisted.
1554    * <p>
1555    * Requires synchronizing on the object returned by
1556    * {@link #getSchemaUpdateLock()}.
1557    *
1558    * @param names the names of the fields to delete
1559    * @return a new IndexSchema based on this schema with the named fields deleted
1560    */
1561   public IndexSchema deleteFields(Collection<String> names) {
1562     String msg = "This IndexSchema is not mutable.";
1563     log.error(msg);
1564     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1565   }
1566 
1567   /**
1568    * Copies this schema, deletes the named field from the copy, creates a new field 
1569    * with the same name using the given args, then rebinds any referring copy fields
1570    * to the replacement field.
1571    *
1572    * <p>
1573    * The schema will not be persisted.
1574    * <p>
1575    * Requires synchronizing on the object returned by {@link #getSchemaUpdateLock()}.
1576    *
1577    * @param fieldName The name of the field to be replaced
1578    * @param replacementFieldType  The field type of the replacement field                                   
1579    * @param replacementArgs Initialization params for the replacement field
1580    * @return a new IndexSchema based on this schema with the named field replaced
1581    */
1582   public IndexSchema replaceField(String fieldName, FieldType replacementFieldType, Map<String,?> replacementArgs) {
1583     String msg = "This IndexSchema is not mutable.";
1584     log.error(msg);
1585     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1586   }
1587 
1588   /**
1589    * Copies this schema, adds the given dynamic fields to the copy,
1590    * Requires synchronizing on the object returned by
1591    * {@link #getSchemaUpdateLock()}.
1592    *
1593    * @param newDynamicFields the SchemaFields to add
1594    * @param copyFieldNames 0 or more names of targets to copy this field to.  The target fields must already exist.
1595    * @param persist to persist the schema or not
1596    * @return a new IndexSchema based on this schema with newDynamicFields added
1597    * @see #newDynamicField(String, String, Map)
1598    */
1599   public IndexSchema addDynamicFields
1600       (Collection<SchemaField> newDynamicFields,
1601        Map<String, Collection<String>> copyFieldNames,
1602        boolean persist) {
1603     String msg = "This IndexSchema is not mutable.";
1604     log.error(msg);
1605     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1606   }
1607 
1608   /**
1609    * Copies this schema, deletes the named dynamic fields from the copy.
1610    * <p>
1611    * The schema will not be persisted.
1612    * <p>
1613    * Requires synchronizing on the object returned by
1614    * {@link #getSchemaUpdateLock()}.
1615    *
1616    * @param fieldNamePatterns the names of the dynamic fields to delete
1617    * @return a new IndexSchema based on this schema with the named dynamic fields deleted
1618    */
1619   public IndexSchema deleteDynamicFields(Collection<String> fieldNamePatterns) {
1620     String msg = "This IndexSchema is not mutable.";
1621     log.error(msg);
1622     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1623   }
1624 
1625   /**
1626    * Copies this schema, deletes the named dynamic field from the copy, creates a new dynamic
1627    * field with the same field name pattern using the given args, then rebinds any referring
1628    * dynamic copy fields to the replacement dynamic field.
1629    *
1630    * <p>
1631    * The schema will not be persisted.
1632    * <p>
1633    * Requires synchronizing on the object returned by {@link #getSchemaUpdateLock()}.
1634    *
1635    * @param fieldNamePattern The glob for the dynamic field to be replaced
1636    * @param replacementFieldType  The field type of the replacement dynamic field                                   
1637    * @param replacementArgs Initialization params for the replacement dynamic field
1638    * @return a new IndexSchema based on this schema with the named dynamic field replaced
1639    */
1640   public ManagedIndexSchema replaceDynamicField
1641       (String fieldNamePattern, FieldType replacementFieldType, Map<String,?> replacementArgs) {
1642     String msg = "This IndexSchema is not mutable.";
1643     log.error(msg);
1644     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1645   }
1646 
1647     /**
1648      * Copies this schema and adds the new copy fields to the copy
1649      * Requires synchronizing on the object returned by
1650      * {@link #getSchemaUpdateLock()}.
1651      *
1652      * @see #addCopyFields(String,Collection,int) to limit the number of copied characters.
1653      *
1654      * @param copyFields Key is the name of the source field name, value is a collection of target field names.  Fields must exist.
1655      * @param persist to persist the schema or not
1656      * @return The new Schema with the copy fields added
1657      */
1658   public IndexSchema addCopyFields(Map<String, Collection<String>> copyFields, boolean persist) {
1659     String msg = "This IndexSchema is not mutable.";
1660     log.error(msg);
1661     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1662   }
1663 
1664   /**
1665    * Copies this schema and adds the new copy fields to the copy.
1666    * 
1667    * Requires synchronizing on the object returned by 
1668    * {@link #getSchemaUpdateLock()}
1669    * 
1670    * @param source source field name
1671    * @param destinations collection of target field names
1672    * @param maxChars max number of characters to copy from the source to each
1673    *                 of the destinations.  Use {@link CopyField#UNLIMITED}
1674    *                 if you don't want to limit the number of copied chars.
1675    * @return The new Schema with the copy fields added
1676    */
1677   public IndexSchema addCopyFields(String source, Collection<String> destinations, int maxChars) {
1678     String msg = "This IndexSchema is not mutable.";
1679     log.error(msg);
1680     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1681   }
1682 
1683   /**
1684    * Copies this schema and deletes the given copy fields from the copy.
1685    * <p>
1686    * The schema will not be persisted.
1687    * <p>
1688    * Requires synchronizing on the object returned by
1689    * {@link #getSchemaUpdateLock()}.
1690    *
1691    * @param copyFields Key is the name of the source field name, value is a collection of target field names. 
1692    *                   Each corresponding copy field directives must exist.
1693    * @return The new Schema with the copy fields deleted
1694    */
1695   public IndexSchema deleteCopyFields(Map<String, Collection<String>> copyFields) {
1696     String msg = "This IndexSchema is not mutable.";
1697     log.error(msg);
1698     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1699   }
1700 
1701 
1702   /**
1703    * Returns a SchemaField if the given fieldName does not already 
1704    * exist in this schema, and does not match any dynamic fields 
1705    * in this schema.  The resulting SchemaField can be used in a call
1706    * to {@link #addField(SchemaField)}.
1707    *
1708    * @param fieldName the name of the field to add
1709    * @param fieldType the field type for the new field
1710    * @param options the options to use when creating the SchemaField
1711    * @return The created SchemaField
1712    * @see #addField(SchemaField)
1713    */
1714   public SchemaField newField(String fieldName, String fieldType, Map<String,?> options) {
1715     String msg = "This IndexSchema is not mutable.";
1716     log.error(msg);
1717     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1718   }
1719 
1720   /**
1721    * Returns a SchemaField if the given dynamic field glob does not already 
1722    * exist in this schema, and does not match any dynamic fields 
1723    * in this schema.  The resulting SchemaField can be used in a call
1724    * to {@link #addField(SchemaField)}.
1725    *
1726    * @param fieldNamePattern the pattern for the dynamic field to add
1727    * @param fieldType the field type for the new field
1728    * @param options the options to use when creating the SchemaField
1729    * @return The created SchemaField
1730    * @see #addField(SchemaField)
1731    */
1732   public SchemaField newDynamicField(String fieldNamePattern, String fieldType, Map<String,?> options) {
1733     String msg = "This IndexSchema is not mutable.";
1734     log.error(msg);
1735     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1736   }
1737 
1738   /**
1739    * Returns the schema update lock that should be synchronized on
1740    * to update the schema.  Only applicable to mutable schemas.
1741    *
1742    * @return the schema update lock object to synchronize on
1743    */
1744   public Object getSchemaUpdateLock() {
1745     String msg = "This IndexSchema is not mutable.";
1746     log.error(msg);
1747     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1748   }
1749 
1750   /**
1751    * Copies this schema, adds the given field type to the copy, then persists the
1752    * new schema.  Requires synchronizing on the object returned by
1753    * {@link #getSchemaUpdateLock()}.
1754    *
1755    * @param fieldType the FieldType to add
1756    * @return a new IndexSchema based on this schema with the new FieldType added
1757    * @see #newFieldType(String, String, Map)
1758    */
1759   public IndexSchema addFieldType(FieldType fieldType) {
1760     String msg = "This IndexSchema is not mutable.";
1761     log.error(msg);
1762     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1763   }
1764 
1765   /**
1766    * Copies this schema, adds the given field type to the copy,
1767    * Requires synchronizing on the object returned by
1768    * {@link #getSchemaUpdateLock()}.
1769    *
1770    * @param fieldTypeList a list of FieldTypes to add
1771    * @param persist to persist the schema or not
1772    * @return a new IndexSchema based on this schema with the new types added
1773    * @see #newFieldType(String, String, Map)
1774    */
1775   public IndexSchema addFieldTypes(List<FieldType> fieldTypeList, boolean persist) {
1776     String msg = "This IndexSchema is not mutable.";
1777     log.error(msg);
1778     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1779   }
1780 
1781   /**
1782    * Copies this schema, deletes the named field types from the copy.
1783    * <p>
1784    * The schema will not be persisted.
1785    * <p>
1786    * Requires synchronizing on the object returned by {@link #getSchemaUpdateLock()}.
1787    *
1788    * @param names the names of the field types to delete
1789    * @return a new IndexSchema based on this schema with the named field types deleted
1790    */
1791   public IndexSchema deleteFieldTypes(Collection<String> names) {
1792     String msg = "This IndexSchema is not mutable.";
1793     log.error(msg);
1794     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1795   }
1796 
1797   /**
1798    * Copies this schema, deletes the named field type from the copy, creates a new field type 
1799    * with the same name using the given args, rebuilds fields and dynamic fields of the given
1800    * type, then rebinds any referring copy fields to the rebuilt fields.
1801    * 
1802    * <p>
1803    * The schema will not be persisted.
1804    * <p>
1805    * Requires synchronizing on the object returned by {@link #getSchemaUpdateLock()}.
1806    *  
1807    * @param typeName The name of the field type to be replaced
1808    * @param replacementClassName The class name of the replacement field type
1809    * @param replacementArgs Initialization params for the replacement field type
1810    * @return a new IndexSchema based on this schema with the named field type replaced
1811    */
1812   public IndexSchema replaceFieldType(String typeName, String replacementClassName, Map<String,Object> replacementArgs) {
1813     String msg = "This IndexSchema is not mutable.";
1814     log.error(msg);
1815     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1816   }
1817 
1818   /**
1819    * Returns a FieldType if the given typeName does not already
1820    * exist in this schema. The resulting FieldType can be used in a call
1821    * to {@link #addFieldTypes(java.util.List, boolean)}.
1822    *
1823    * @param typeName the name of the type to add
1824    * @param className the name of the FieldType class
1825    * @param options the options to use when creating the FieldType
1826    * @return The created FieldType
1827    * @see #addFieldTypes(java.util.List, boolean)
1828    */
1829   public FieldType newFieldType(String typeName, String className, Map<String,?> options) {
1830     String msg = "This IndexSchema is not mutable.";
1831     log.error(msg);
1832     throw new SolrException(ErrorCode.SERVER_ERROR, msg);
1833   }
1834 
1835   protected String getFieldTypeXPathExpressions() {
1836     //               /schema/fieldtype | /schema/fieldType | /schema/types/fieldtype | /schema/types/fieldType
1837     String expression = stepsToPath(SCHEMA, FIELD_TYPE.toLowerCase(Locale.ROOT)) // backcompat(?)
1838         + XPATH_OR + stepsToPath(SCHEMA, FIELD_TYPE)
1839         + XPATH_OR + stepsToPath(SCHEMA, TYPES, FIELD_TYPE.toLowerCase(Locale.ROOT))
1840         + XPATH_OR + stepsToPath(SCHEMA, TYPES, FIELD_TYPE);
1841     return expression;
1842   }
1843 }